Skip to content

Make focal output dtype consistent across backends#3226

Merged
brendancol merged 6 commits into
mainfrom
deep-sweep-metadata-focal-2026-06-10-01
Jun 11, 2026
Merged

Make focal output dtype consistent across backends#3226
brendancol merged 6 commits into
mainfrom
deep-sweep-metadata-focal-2026-06-10-01

Conversation

@brendancol

Copy link
Copy Markdown
Contributor

Closes #3217

Backend coverage: numpy, cupy, dask+numpy, dask+cupy all verified live on this host (CUDA available).

Test plan:

  • New parametrized tests: test_mean_dtype_consistent_across_backends_3217 (4 backends x 3 input dtypes), test_apply_dask_advertised_dtype_matches_computed_3217, test_focal_stats_dask_advertised_dtype_matches_computed_3217 (dask backends x 3 input dtypes), and test_mean_gpu_matches_cpu_float64_3217 (exact CPU/GPU value parity).
  • Full test_focal.py suite: 258 passed.

mean() cast to float32 on the cupy and dask+cupy paths while the CPU
paths returned float64, losing precision on the GPU. The dask paths of
mean, apply, and focal_stats also passed an untyped meta to
map_overlap, so the lazy dtype advertised float64 while compute()
returned float32 for float32/int input. Keep the dispatched dtype on
the GPU mean paths and type every map_overlap meta with data.dtype.
Adds parametrized regression tests over all four backends.
@github-actions github-actions Bot added the performance PR touches performance-sensitive code label Jun 10, 2026

@brendancol brendancol left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Review: Make focal output dtype consistent across backends

Blockers (must fix before merge)

None.

Suggestions (should fix, not blocking)

  • xrspatial/focal.py:385-402: the mean() docstring's cupy example still shows output computed under the old float32 cast (array([[0.47928995, ...]]) printed at float32 precision) and an outdated cupy.core.core.ndarray class path. With this PR the GPU path computes in float64, so the example no longer matches what a user would see. Worth refreshing while you are touching this contract.

Nits (optional improvements)

  • xrspatial/tests/test_focal.py (new _computed_dtype helper): the helper is defined mid-file between test sections. Fine as is, but the general_checks.py module is where the other cross-backend helpers live if it ever gets a second user.

What looks good

  • The fix targets exactly the five sites that produce or advertise the wrong dtype, and nothing else. hotspots() meta sites were already typed and are untouched.
  • _mean_cupy keeps the device array and inherits the dispatched dtype instead of forcing float32; the new test asserts exact (not approximate) CPU/GPU equality in float64, which holds because both loops accumulate sequentially.
  • Tests cover 4 backends x 3 input dtypes for mean() and both dask backends x 3 dtypes for apply()/focal_stats(), asserting advertised dtype == computed dtype, which is the actual regression.
  • 4x4 input with (2, 2) chunks exercises multi-chunk map_overlap, so the typed meta is checked against real chunk boundaries.

Checklist

  • Algorithm matches reference (no algorithm change; dtype plumbing only)
  • All implemented backends produce consistent results (verified live on CUDA host)
  • NaN handling unchanged and covered by existing suite (258 passed)
  • Edge cases covered (float64/float32/int32 inputs)
  • Dask chunk boundaries handled correctly (typed meta, depth unchanged)
  • No premature materialization (meta typing is graph-construction only)
  • Benchmark not needed (bug fix, no new function); note GPU mean now runs in float64, so a throughput drop on large GPU rasters is expected and intentional
  • README feature matrix not applicable
  • Docstrings: see suggestion about the stale cupy example

@brendancol brendancol left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow-up review (after 1f20224)

The suggestion from the first pass is addressed: the mean() cupy docstring example now shows the float64 output (0.47928994, verified by running the example on a CUDA host) and the current cupy.ndarray class path.

Disposition of first-pass findings:

  • Suggestion (stale cupy docstring example): fixed in 1f20224.
  • Nit (_computed_dtype helper location): left in test_focal.py. It has a single consumer; moving a 4-line helper to general_checks.py before a second user exists adds indirection for no benefit.

No new findings. Full focal suite still passes (258).

…ocal-2026-06-10-01

Conflicts:
- xrspatial/focal.py: main's #3221 (issue #3214, a duplicate of #3217)
  changed mean() to the _promote_float contract; kept main's GPU/dask
  cast lines, this branch keeps the typed map_overlap metas.
- xrspatial/tests/test_focal.py: aligned the 3217 mean dtype test with
  the _promote_float contract (float32 in -> float32 out).
- .claude/sweep-metadata-state.csv: restored LF line endings, kept
  main's geotiff row and this branch's focal row (notes updated).
@brendancol brendancol merged commit aace125 into main Jun 11, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

performance PR touches performance-sensitive code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

focal: mean() dtype differs by backend; apply()/focal_stats() dask meta advertises float64 but computes float32

1 participant